package gu.sql2java.store;

import static gu.sql2java.store.BinaryUtils.saveBytes;

import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * 二进制数据本地存储实现
 * 
 * @author guyadong
 *
 */
public class LocalBinaryStore extends BasesLocalBinaryStore {
	private static final String ORIGIN_ROOT_NAME = "origin";
	public static final String PROTOCOL = "lbs";
	/** 分区名为'origin',存储级数为2的单实例 */
	public static final LocalBinaryStore SINGLETON = new LocalBinaryStore();
	/** 存储路径前缀(分区) */
	private final String partition;
	/** 存储分区名(前后带'/') */
	private final String _spartition;
	/** 存储目录级数 */
	private final int level;

	private volatile Integer rootPathLength;

	private LocalBinaryStore() {
		this((File) null, ORIGIN_ROOT_NAME, 2);
	}

	/**
	 * @param storeRoot 存储根路径
	 * @param partition 存储路径前缀(分区),为{@code null}或空则使用默认值'origin'
	 * @param level     存储目录级数,小于0或大于4则使用默认值2
	 * @see #levelFolderOf(String, String)
	 */
	public LocalBinaryStore(File storeRoot, String partition, int level) {
		super(storeRoot);
		this.partition = (null == partition || partition.length() == 0) ? ORIGIN_ROOT_NAME : partition;
		this._spartition = slashify(partition, true);
		this.level = (level < 0 || level > 4) ? 2 : level;
	}

	/**
	 * @see #LocalBinaryStore(File, String, int)
	 */
	public LocalBinaryStore(String storeRoot, String partition, int level) {
		this(new File(storeRoot), partition, level);
	}

	private int getRootPathLength() {
		if (rootPathLength == null) {
			synchronized (this) {
				if (rootPathLength == null) {
					rootPathLength = slashify(getStoreRoot().getAbsolutePath(), true).length();
				}
			}
		}
		return rootPathLength;
	}

	/**
	 * 创建存储地址对象(将path中storeRoot路径剥离)
	 * 
	 * @param file
	 * @return 存储地址URL
	 */
	protected URL createStoreURL(File file) {
		try {
			String path = slashify(file.getAbsolutePath(), false);
			return new URL(PROTOCOL, null, slashify(path.substring(getRootPathLength()), false));
		} catch (MalformedURLException e) {
			throw new RuntimeException(e);
		}
	}

	@Override
	protected URL doFind(final String md5) {
		File folder = localFolderOf(md5);
		if (folder.isDirectory()) {
			File[] files = folder.listFiles(new FilenameFilter() {
				@Override
				public boolean accept(File dir, String name) {
					return name.startsWith(md5);
				}
			});
			return files.length > 0 ? createStoreURL(files[0]) : null;
		}
		return null;
	}

	@Override
	protected URL doStore(byte[] binary, String md5, String extension, boolean overwrite, boolean makeURLOnly) throws IOException {
		File dst = createDestFile(md5, extension);
		if (!makeURLOnly) {
			if (dst.length() != binary.length) {
				if(!dst.isFile() || overwrite) {
					saveBytes(binary, dst, true);
				}
			}
		}
		return createStoreURL(dst);
	}

	/**
	 * 返回存储URL相对路径
	 * 
	 * @param storedUrl
	 * @param prefix    需要替换的路径前缀， 为{@code null}直接返回URL的 path部分
	 */
	public String asRelativePath(URL storedUrl, String prefix) {
		if (null != storedUrl) {
			String path = storedUrl.getPath();
			if (null != prefix && prefix.length() > 0) {
				path = slashify(prefix, true) + path.substring(_spartition.length());
			}
			return path;
		}
		return null;
	}

	/**
	 * 将存储URL替换路径前缀，转为protocol,host,port指定的标准 URL
	 * 
	 * @param storedUrl
	 * @param protocol  为{@code null}则默认为http
	 * @param host
	 * @param port      为{@code null}为默认端口
	 * @param prefix
	 * @see #asRelativePath(URL, String)
	 */
	public URL asCanonicalURL(URL storedUrl, String protocol, String host, Integer port, String prefix) {
		if (null == host) {
			throw new NullPointerException("hostAndPort is null");
		}
		String path = asRelativePath(storedUrl, prefix);
		if (null != path) {
			if (null == protocol) {
				protocol = "http";
			}
			try {
				if (null != port && port > 0) {
					return new URL(protocol, host, port, path);
				}
				return new URL(protocol, host, path);
			} catch (MalformedURLException e) {
				throw new RuntimeException(e);
			}
		}
		return null;
	}

	/**
	 * 将输入的URL转转为本地存储URL
	 * 
	 * @param canonicalUrl
	 */
	public URL asStoredURL(URL canonicalUrl) {
		if (null != canonicalUrl) {
			String path = canonicalUrl.getPath();
			if (path != null) {
				int rpos = matchRelativePath(path);
				if (rpos >= 0) {
					path = this._spartition + path.substring(rpos);
				}
				try {
					return new URL(PROTOCOL, null, path);
				} catch (MalformedURLException e) {
					throw new RuntimeException(e);
				}
			}
		}
		return null;
	}

	public LocalBinaryStore setStoreRoot(File storeRoot) {
		this.storeRoot = storeRoot;
		return this;
	}

	public String getPartition() {
		return partition;
	}

	/**
	 * 根据 {@link #level}指定的存储目录级数生成MD5对应的存储文件夹名
	 * 
	 * @param prefix 路径前缀
	 * @param md5
	 */
	private Path levelFolderOf(String prefix, String md5) {
		String[] subfolders = new String[level];
		for (int i = 0, j = 0; i < level; ++i, j += 2) {
			subfolders[i] = md5.substring(j, j + 2);
		}
		return Paths.get(prefix, subfolders);
	}

	protected String relativeFilePath(String md5, String suffix) {
		// md5前两位做第一级子目录，接下来的两位做第二级子目录
		String s;
		if ((suffix == null || suffix.length() == 0)) {
			s = "";
		} else if (suffix.charAt(0) != '.') {
			s = "." + suffix;
		} else {
			s = suffix;
		}
		Path p = Paths.get(levelFolderOf(_spartition, md5).toString(), md5 + s);
		return p.toString();
	}

	/** 根据MD5计算出对应的存储路径 */
	protected File localFolderOf(String md5) {
		// md5前两位做第一级子目录，接下来的两位做第二级子目录
		return Paths.get(getStoreRoot().getAbsolutePath(), levelFolderOf(_spartition, md5).toString()).toFile();
	}

	/**
	 * 正则表达匹配判断是路径后部否为本地存储路径(), 如{@code 8b/ce/8bce0bb6de7e5c025a4a06e3d05edce7.tx}
	 * 如果是则返回匹配的相对路径的起始位置,否则返回-1
	 */
	protected int matchRelativePath(String path) {
		if (null != path && path.length() > 0) {
			Matcher matcher = Pattern.compile("([\\da-f]{2}/){" + level + "}[\\da-f]{32}(\\.\\w*)?$").matcher(path);
			if (matcher.find()) {
				return matcher.start();
			}
		}
		return -1;
	}

	private File createDestFile(String md5, String suffix) {
		return new File(getStoreRoot(), relativeFilePath(md5, suffix));
	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = super.hashCode();
		result = prime * result + Objects.hash(partition);
		return result;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (!super.equals(obj))
			return false;
		if (getClass() != obj.getClass())
			return false;
		LocalBinaryStore other = (LocalBinaryStore) obj;
		return Objects.equals(partition, other.partition);
	}

	@Override
	public String toString() {
		StringBuilder builder = new StringBuilder();
		builder.append("LocalBinaryStore [partition=").append(partition).append(", storeRoot=").append(storeRoot)
				.append("]");
		return builder.toString();
	}

	private static String slashify(String path, boolean isDirectory) {
		String p = path;
		if (File.separatorChar != '/')
			p = p.replace(File.separatorChar, '/');
		if (!p.startsWith("/"))
			p = "/" + p;
		if (!p.endsWith("/") && isDirectory)
			p = p + "/";
		return p;
	}
}
